package jp.reflexworks.gdata.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;

import com.google.gdata.client.DocumentQuery;
import com.google.gdata.client.docs.DocsService;
import com.google.gdata.client.media.ResumableGDataFileUploader;
import com.google.gdata.client.uploader.ProgressListener;
import com.google.gdata.client.GoogleService.SessionExpiredException;
import com.google.gdata.data.MediaContent;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.acl.AclEntry;
import com.google.gdata.data.acl.AclFeed;
import com.google.gdata.data.acl.AclRole;
import com.google.gdata.data.acl.AclScope;
import com.google.gdata.data.media.MediaByteArraySource;
import com.google.gdata.data.media.MediaFileSource;
import com.google.gdata.data.media.MediaSource;
import com.google.gdata.data.docs.DocumentEntry;
import com.google.gdata.data.docs.DocumentListEntry;
import com.google.gdata.data.docs.DocumentListFeed;
import com.google.gdata.data.docs.FolderEntry;
import com.google.gdata.data.docs.PresentationEntry;
import com.google.gdata.data.docs.SpreadsheetEntry;
import com.google.gdata.util.ServiceException;
import com.google.gdata.util.InvalidEntryException;
import com.google.gdata.util.AuthenticationException;

public class GoogleDocsUtil {

	public static final String GOOGLE_DOCS_URL = "https://docs.google.com/feeds/default/private/full/";
	public static final String FOLDER_PATH = "-/folder";
	public static final String CONTENTS_PATH = "/contents";
	public static final String DELETE_OPTION = "?delete=true";
	public static final String CONVERT_FALSE_OPTION = "?convert=false";

	public static final String MIME_TYPE_PDF = "application/pdf";

	public static final String DOCUMENT = "document";
	public static final String PRESENTATION = "presentation";
	public static final String SPREADSHEET = "spreadsheet";
	
	public static final int NUM_RETRIES = 5;
	public static final int SLEEP_MILL_SECOND = 200;

	public static final int CONNECT_TIMEOUT = 30;	// in seconds

	public static final String EXPORT_URL = "https://docs.google.com/MiscCommands?command=saveasdoc&docID=";
	public static final String EXPORT_FORMAT = "&exportFormat=";
	public static final String EXPORT_FORMAT_TXT = "txt";
	
	public static final String TIMEOUT_MESSAGE = "Timeout while fetching";
	
	/** Default create-media-url for uploading documents */
	private static final String DEFAULT_RESUMABLE_UPLOAD_URL =
		"https://docs.google.com/feeds/upload/create-session/default/private/full/";

	/** Maximum number of concurrent uploads */
	private static final int MAX_CONCURRENT_UPLOADS = 10;

	/** Time interval at which upload task will notify about the progress */
	private static final int PROGRESS_UPDATE_INTERVAL = 1000;

	/** Max size for each upload chunk */
	//private static final int DEFAULT_CHUNK_SIZE = 10000000;
	//private static final int DEFAULT_CHUNK_SIZE = 1048576;	// 1M	
	private static final int DEFAULT_CHUNK_SIZE = 10485760;	// 10M

	private String appName;
	private String email;
	private String password;
	private String token;
	private int connectTimeout = -1;

	Logger logger = Logger.getLogger(this.getClass().getName());

	private DocsService service = null;

	/**
	 * Client Login
	 */
	public GoogleDocsUtil(String appName, String email, String password) {
		this.appName = appName;
		this.email = email;
		this.password = password;
	}

	/**
	 * AuthSub
	 */
	public GoogleDocsUtil(String appName, String token) {
		this.appName = appName;
		this.token = token;
	}

	public DocsService login() throws AuthenticationException {
		DocsService service = null;
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				service = new DocsService(appName);
				if (token != null) {
					service.setAuthSubToken(token, null);
				} else {
					service.setUserCredentials(email, password);
				}
				this.service = service;
				return service;

			} catch (AuthenticationException e) {
				logger.info("AuthenticationException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		return null;
	}

	public DocumentListEntry uploadFile(File file, DocumentListEntry folder, String title)
    throws IOException, ServiceException {
		return uploadFile(file, folder, title, MIME_TYPE_PDF, true);
	}

	public DocumentListEntry uploadFile(File file, DocumentListEntry folder, String title, boolean isNotConvert)
    throws IOException, ServiceException {
		return uploadFile(file, folder, title, MIME_TYPE_PDF, isNotConvert);
	}
	
	public DocumentListEntry uploadFile(File file, DocumentListEntry folder, String title, String mimeType,
			boolean isNotConvert)
    throws IOException, ServiceException {
		DocumentListEntry newDocument = new DocumentListEntry();
		newDocument.setFile(file, mimeType);
		newDocument.setTitle(new PlainTextConstruct(title));

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				return service.insert(new URL(getFolderUrl(folder, isNotConvert)), newDocument);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				// タイムアウトの場合、処理が継続されているのでリトライしない
				if (e.getMessage().indexOf(TIMEOUT_MESSAGE) > -1) {
					return null;
				}
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		return null;
	}

	public DocumentListEntry createFolder(File file, DocumentListEntry folder, String title) 
	throws IOException, ServiceException {
		DocumentListEntry newEntry = new FolderEntry();
		newEntry.setTitle(new PlainTextConstruct(title));

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				return service.insert(new URL(getFolderUrl(folder)), newEntry);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				// タイムアウトの場合、処理が継続されているのでリトライしない
				if (e.getMessage().indexOf(TIMEOUT_MESSAGE) > -1) {
					return null;
				}
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		return null;
	}

	public DocumentListEntry createFolder(String folderName, String parentResourceId) 
	throws IOException, ServiceException {
		DocumentListEntry newEntry = new FolderEntry();
		newEntry.setTitle(new PlainTextConstruct(folderName));

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				return service.insert(new URL(getFolderUrl(parentResourceId)), newEntry);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				// タイムアウトの場合、処理が継続されているのでリトライしない
				if (e.getMessage().indexOf(TIMEOUT_MESSAGE) > -1) {
					return null;
				}
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		return null;
	}
	
	public String getFolderUrl(DocumentListEntry folder) {
		return getFolderUrl(folder, false);
	}
	
	public String getFolderUrl(DocumentListEntry folder, boolean isNotConvert) {
		String resourceId = null;
		if (folder != null) {
			resourceId = folder.getResourceId();
		}
		return getFolderUrl(resourceId, isNotConvert);
	}
	
	public String getFolderUrl(String resourceId) {
		return getFolderUrl(resourceId, false);
	}
	
	/*
	 * 親フォルダ指定のURLを取得する
	 * @param resourceId 親フォルダのResourceId
	 */
	public String getFolderUrl(String resourceId, boolean isNotConvert) {
		StringBuffer url = new StringBuffer(GOOGLE_DOCS_URL);
		if (resourceId != null) {
			url.append(resourceId);
			url.append(CONTENTS_PATH);
		}
		if (isNotConvert) {
			url.append(CONVERT_FALSE_OPTION);
		}
		return url.toString();
	}
	
	/*
	 * Resume UploadのURLを取得する
	 * @param folder 親フォルダのEntry
	 */
	public String getResumeUploadFolderUrl(DocumentListEntry folder, boolean isNotConvert) {
		String resourceId = null;
		if (folder != null) {
			resourceId = folder.getResourceId();
		}
		return getResumeUploadFolderUrl(resourceId, isNotConvert);
	}
	
	/*
	 * Resume UploadのURLを取得する
	 * @param resourceId 親フォルダのResourceId
	 */
	public String getResumeUploadFolderUrl(String resourceId, boolean isNotConvert) {
		StringBuffer url = new StringBuffer(DEFAULT_RESUMABLE_UPLOAD_URL);
		if (resourceId != null) {
			url.append(resourceId);
			url.append(CONTENTS_PATH);
		}
		if (isNotConvert) {
			url.append(CONVERT_FALSE_OPTION);
		}
		return url.toString();
	}
	
	/*
	 * フォルダ検索用のURLを取得する
	 * @param resourceId 親フォルダのResourceId
	 */
	public String getSearchFolderUrl(String resourceId) {
		StringBuffer urlStr = new StringBuffer();
		urlStr.append(getFolderUrl(resourceId));
		if (resourceId != null) {
			urlStr.append("/");
		}
		urlStr.append(FOLDER_PATH);
		return urlStr.toString();
	}
	
	/*
	 * エクスポートURLを取得する
	 */
	public String getExportUrl(String docId, String format) {
		StringBuffer urlStr = new StringBuffer();
		urlStr.append(EXPORT_URL);
		urlStr.append(docId);
		if (format != null) {
			urlStr.append(EXPORT_FORMAT);
			urlStr.append(format);
		}
		return urlStr.toString();
	}
	
	/*
	 * フォルダを検索する
	 * @param title フォルダのタイトル
	 */
	public DocumentListEntry retrieveFolder(String title, boolean excludeTrash) 
	throws IOException, ServiceException {
	    return retrieveFolder(title, null, excludeTrash);
	}
	
	/*
	 * 指定されたフォルダ配下のフォルダを取得する
	 * @param title フォルダのタイトル
	 * @param parentResourceId フォルダのResourceId
	 */
	public DocumentListEntry retrieveFolder(String title, String parentResourceId, boolean excludeTrash) 
	throws IOException, ServiceException {
		boolean exact = false;
		if (parentResourceId == null) {
			exact = true;
		}
		
	    URL feedUri = new URL(getSearchFolderUrl(parentResourceId));
	    List<DocumentListEntry> entries = retrieveDocument(feedUri, title, exact, excludeTrash);
		if (entries != null && entries.size() > 0) {
			if (entries.size() != 1) {
				logger.warning(title + " entries.size = " + entries.size());
			}
			return entries.get(0);
		}
		return null;
	}
	
	/*
	 * 指定されたフォルダ配下のフォルダ一覧を取得する
	 * @param entry 親フォルダのEntry
	 */
	public DocumentListFeed retrieveFolders(DocumentListEntry entry, boolean excludeTrash) 
	throws IOException, ServiceException {
		String parentResourceId = null;
		if (entry != null) {
			parentResourceId = entry.getResourceId();
		}
		return retrieveFolders(parentResourceId, excludeTrash);
	}
	
	/*
	 * 指定されたフォルダ配下のフォルダ一覧を取得する
	 * @param parentResourceId 親フォルダのResourceId
	 */
	public DocumentListFeed retrieveFolders(String parentResourceId, boolean excludeTrash) 
	throws IOException, ServiceException {
	    URL feedUri = new URL(getSearchFolderUrl(parentResourceId));
	    return retrieveDocumentFeed(feedUri, null, false, excludeTrash);
	}
	
	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param title ファイルのタイトル
	 * @param parentResourceId フォルダのResourceId
	 */
	public DocumentListEntry retrieveFile(String title, String parentResourceId, boolean excludeTrash) 
	throws IOException, ServiceException {
		boolean exact = false;
		if (parentResourceId == null) {
			exact = true;
		}
		StringBuffer urlStr = new StringBuffer();
		urlStr.append(getFolderUrl(parentResourceId));
	    URL feedUri = new URL(urlStr.toString());
	    List<DocumentListEntry> entries = retrieveDocument(feedUri, title, exact, excludeTrash);
		if (entries != null && entries.size() > 0) {
			if (entries.size() != 1) {
				logger.warning(title + " entries.size = " + entries.size());
			}
			return entries.get(0);
		}
		return null;
	}
	
	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param title ファイルのタイトル
	 * @param parentResourceId フォルダのResourceId
	 */
	public DocumentListEntry retrieveFile(String title, String parentResourceId, 
			boolean exact, boolean excludeTrash) 
	throws IOException, ServiceException {
		StringBuffer urlStr = new StringBuffer();
		urlStr.append(getFolderUrl(parentResourceId));
	    URL feedUri = new URL(urlStr.toString());
	    List<DocumentListEntry> entries = retrieveDocument(feedUri, title, exact, excludeTrash);
		if (entries != null && entries.size() > 0) {
			if (entries.size() != 1) {
				logger.warning(title + " entries.size = " + entries.size());
			}
			return entries.get(0);
		}
		return null;
	}
	
	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param title ファイルのタイトル
	 * @param parentResourceId フォルダのResourceId
	 * @param excludeTrash true:ゴミ箱にあるファイルは抽出しない、false:ゴミ箱にあるファイルも抽出する
	 */
	public List<DocumentListEntry> retrieveFileList(String title, String parentResourceId, 
			boolean excludeTrash) 
	throws IOException, ServiceException {
		boolean exact = false;
		if (parentResourceId == null) {
			exact = true;
		}
		StringBuffer urlStr = new StringBuffer();
		urlStr.append(getFolderUrl(parentResourceId));
	    URL feedUri = new URL(urlStr.toString());
	    return retrieveDocument(feedUri, title, exact, excludeTrash);
	}
	
	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param title ファイルのタイトル
	 * @param parentResourceId フォルダのResourceId
	 * @param excludeTrash true:ゴミ箱にあるファイルは抽出しない、false:ゴミ箱にあるファイルも抽出する
	 */
	public List<DocumentListEntry> retrieveFileList(String title, String parentResourceId, 
			boolean exact, boolean excludeTrash) 
	throws IOException, ServiceException {
		StringBuffer urlStr = new StringBuffer();
		urlStr.append(getFolderUrl(parentResourceId));
	    URL feedUri = new URL(urlStr.toString());
	    return retrieveDocument(feedUri, title, exact, excludeTrash);
	}
	
	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param entry 親フォルダのentry
	 */
	public DocumentListFeed retrieveFiles(DocumentListEntry entry, boolean excludeTrash) 
	throws IOException, ServiceException {
		return retrieveFiles(entry, null, excludeTrash);
	}
	
	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param entry 親フォルダのentry
	 */
	public DocumentListFeed retrieveFiles(DocumentListEntry entry, String nextLinkHref, boolean excludeTrash) 
	throws IOException, ServiceException {
		String parentResourceId = null;
		if (entry != null) {
			parentResourceId = entry.getResourceId();
		}
		return retrieveFiles(parentResourceId, nextLinkHref, excludeTrash);
	}

	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param parentResourceId フォルダのResourceId
	 */
	public DocumentListFeed retrieveFiles(String parentResourceId, boolean excludeTrash) 
	throws IOException, ServiceException {
		URL feedUri = null;
		StringBuffer urlStr = new StringBuffer();
		urlStr.append(getFolderUrl(parentResourceId));
		feedUri = new URL(urlStr.toString());
	    return retrieveDocumentFeed(feedUri, null, false, null, excludeTrash);
	}
	
	/*
	 * 指定されたフォルダ配下のファイルを取得する
	 * @param parentResourceId フォルダのResourceId
	 */
	public DocumentListFeed retrieveFiles(String parentResourceId, String nextLinkHref, boolean excludeTrash) 
	throws IOException, ServiceException {
		URL feedUri = null;
		if (nextLinkHref == null) {
			StringBuffer urlStr = new StringBuffer();
			urlStr.append(getFolderUrl(parentResourceId));
			feedUri = new URL(urlStr.toString());
		}
	    return retrieveDocumentFeed(feedUri, null, false, nextLinkHref, excludeTrash);
	}
	
	public List<DocumentListEntry> retrieveDocument(URL feedUri, String title, boolean exact, boolean excludeTrash) 
	throws IOException, ServiceException {
		DocumentListFeed feed = retrieveDocumentFeed(feedUri, title, exact, excludeTrash);
		if (feed != null) {
			return feed.getEntries();
		}
		return null;
	}
	
	public DocumentListFeed retrieveDocumentFeed(URL feedUri, String title, boolean exact, boolean excludeTrash) 
	throws IOException, ServiceException {
		return retrieveDocumentFeed(feedUri, title, exact, null, excludeTrash);
	}
	
	public DocumentListFeed retrieveDocumentFeed(String nextLinkHref, boolean excludeTrash) 
	throws IOException, ServiceException {
		return retrieveDocumentFeed(null, null, false, nextLinkHref, excludeTrash);
	}
	
	public DocumentListFeed retrieveDocumentFeed(URL feedUri, String title, boolean exact, 
			String nextLinkHref, boolean excludeTrash) 
	throws IOException, ServiceException {
		DocumentListFeed feed = null;

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		if (nextLinkHref != null) {
			for (int r = 0; r < NUM_RETRIES; r++) {
				try {
					feed = service.getFeed(new URL(nextLinkHref), DocumentListFeed.class);
					break;

				} catch (IOException e) {
					logger.info("IOException:"+e.getMessage());
					if (r == (NUM_RETRIES - 1)) {
						throw e;
					}
					sleep(SLEEP_MILL_SECOND);

				} catch(SessionExpiredException e) {
					logger.info("SessionExpiredException:"+e.getMessage());
					login();

				} catch (ServiceException e) {
					logger.info("ServiceException:"+e.getMessage());
					if (r == (NUM_RETRIES - 1)) {
						throw e;
					}
					sleep(SLEEP_MILL_SECOND);
				}
			}
		} else {
		    DocumentQuery query = new DocumentQuery(feedUri);
		    if (title != null) {
			    query.setTitleQuery(title);
			    query.setTitleExact(exact);	// setTitleExactをtrueにするとフォルダ階層が指定されている場合、無視されるので注意
		    }
			if (connectTimeout > 0) {
				service.setConnectTimeout(connectTimeout);
			}
			for (int r = 0; r < NUM_RETRIES; r++) {
				try {
					feed = service.getFeed(query, DocumentListFeed.class);
					break;

				} catch (IOException e) {
					logger.info("IOException:"+e.getMessage());
					if (r == (NUM_RETRIES - 1)) {
						throw e;
					}
					sleep(SLEEP_MILL_SECOND);

				} catch(SessionExpiredException e) {
					logger.info("SessionExpiredException:"+e.getMessage());
					login();

				} catch (ServiceException e) {
					logger.info("ServiceException:"+e.getMessage());
					if (r == (NUM_RETRIES - 1)) {
						throw e;
					}
					sleep(SLEEP_MILL_SECOND);
				}
			}
		}

		List<DocumentListEntry> entries = new ArrayList<DocumentListEntry>();
		for (DocumentListEntry entry : feed.getEntries()) {
			boolean isAdd = true;
			/*
			if (title != null) {
				String docsTitle = entry.getTitle().getPlainText();
				int idx = docsTitle.indexOf(Constants.TITLE_SEPARATOR);
				if (idx > 0) {
					docsTitle = docsTitle.substring(0, idx);
				}
				if (!title.equals(docsTitle)) {
					isAdd = false;
				}
			}
			*/
			if (excludeTrash && entry.isTrashed()) {
				isAdd = false;
			}
			
			if (isAdd) {
				entries.add(entry);
			}
		}
		feed.setEntries(entries);
		
		return feed;
	}
	
	public List<DocumentListEntry> fullTextQuery(String word) 
	throws IOException, ServiceException {
	    URL feedUri = new URL(GOOGLE_DOCS_URL);
	    DocumentQuery query = new DocumentQuery(feedUri);
	    query.setFullTextQuery(word);

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
	    for (int r = 0; r < NUM_RETRIES; r++) {
			try {
			    DocumentListFeed feed = service.getFeed(query, DocumentListFeed.class);
			    return feed.getEntries();

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}
	
	public DocumentListEntry getEntryByResourceId(String resourceId, boolean excludeTrash) 
	throws IOException, ServiceException {
		URL url = new URL(GOOGLE_DOCS_URL + resourceId);

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				DocumentListEntry entry =  service.getEntry(url, DocumentListEntry.class);
				if (excludeTrash && entry.isTrashed()) {
					return null;
				}
				return entry;

			} catch (InvalidEntryException e) {
				return null;

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}

	public void downloadFileByEntry(DocumentListEntry entry, OutputStream out)
	throws IOException, ServiceException {
		MediaContent mc = (MediaContent)entry.getContent();
		downloadFile(mc, out);
	}
	
	public void downloadFileByResourceId(String resourceId, OutputStream out)
	throws IOException, ServiceException {
		DocumentListEntry entry = null;

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				entry = service.getEntry(new URL(GOOGLE_DOCS_URL + resourceId), DocumentListEntry.class);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		downloadFileByEntry(entry, out);
	}

	public void downloadFile(String exportUrl, OutputStream out)
	throws IOException, ServiceException {
		MediaContent mc = new MediaContent();
		mc.setUri(exportUrl);
		downloadFile(mc, out);
	}

	public void downloadFile(MediaContent mc, OutputStream out)
	throws IOException, ServiceException {
		MediaSource ms = null;

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				ms = service.getMedia(mc);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		if (ms != null) {
			BufferedInputStream bis = null;
			try {
				bis = new BufferedInputStream(ms.getInputStream());
	
				int c;
				while ((c = bis.read()) != -1) {
					out.write(c);
				}
	
			} finally {
				if (bis != null) {
					bis.close();
				}
				if (out != null) {
					out.flush();
					out.close();
				}
			}
		}
	}
	
	public InputStream getDownloadFileStreamByEntry(DocumentListEntry entry)
	throws IOException, ServiceException {
		MediaContent mc = (MediaContent)entry.getContent();
		return getDownloadFileStream(mc);
	}
	
	public InputStream getDownloadFileStreamByResourceId(String resourceId)
	throws IOException, ServiceException {
		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				DocumentListEntry entry = service.getEntry(new URL(GOOGLE_DOCS_URL + resourceId), DocumentListEntry.class);
				return getDownloadFileStreamByEntry(entry);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}

	public InputStream getDownloadFileStream(String exportUrl)
	throws IOException, ServiceException {
		MediaContent mc = new MediaContent();
		mc.setUri(exportUrl);
		return getDownloadFileStream(mc);
	}

	public InputStream getDownloadFileStream(MediaContent mc)
	throws IOException, ServiceException {
		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				MediaSource ms = service.getMedia(mc);
				return ms.getInputStream();

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}
	
	/*
	 * ドキュメントを削除する
	 */
	public void deleteDocument(DocumentListEntry entry) throws IOException, ServiceException {
		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				//service.delete(new URL(GOOGLE_DOCS_URL + entry.getResourceId()), "*");
			    //service.delete(new URL(entry.getEditLink().getHref() + DELETE_OPTION), entry.getEtag());
			    service.delete(new URL(entry.getEditLink().getHref() + DELETE_OPTION), "*");
				break;

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
	}

	/*
	 * 新規ドキュメントを作成する
	 */
	public DocumentListEntry createNewDocument(String title, String docType, String url, 
			byte[] content, String contentType)
	throws IOException, ServiceException {
		DocumentListEntry entry = createNewDocument(title, docType, url);
		if (entry != null) {
			try {
				service.getRequestFactory().setHeader("If-Match", "*");
				return updateMedia(entry, contentType, content);
				
			} finally {
				service.getRequestFactory().setHeader("If-Match", null);
			}
		}
		
		return null;
	}

	/*
	 * 空のドキュメントを作成する
	 */
	public DocumentListEntry createNewDocument(String title, String type, String url)
	throws IOException, ServiceException {
		DocumentListEntry newEntry = null;
		if (type.equals(DOCUMENT)) {
			newEntry = new DocumentEntry();
		} else if (type.equals(PRESENTATION)) {
			newEntry = new PresentationEntry();
		} else if (type.equals(SPREADSHEET)) {
			newEntry = new SpreadsheetEntry();
		} else {
			throw new IllegalArgumentException("Please specify either document, spreadsheet or presentation for a type.");
		}
		newEntry.setTitle(new PlainTextConstruct(title));

		// Prevent collaborators from sharing the document with others?
		// newEntry.setWritersCanInvite(false);

		// You can also hide the document on creation
		// newEntry.setHidden(true);

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				return service.insert(new URL(url), newEntry);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				// タイムアウトの場合、処理が継続されているのでリトライしない
				if (e.getMessage().indexOf(TIMEOUT_MESSAGE) > -1) {
					return null;
				}
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}
	
	/*
	 * コンテンツに書き込む
	 */
	public DocumentListEntry updateMedia(DocumentListEntry entry, String contentType, byte[] content)
	throws IOException, ServiceException {
		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				entry.setMediaSource(new MediaByteArraySource(content, contentType));
				return entry.updateMedia(true);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				// タイムアウトの場合、処理が継続されているのでリトライしない
				if (e.getMessage().indexOf(TIMEOUT_MESSAGE) > -1) {
					return null;
				}
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}

	/*
	 * 新規ドキュメントを作成する
	 * アップロード機能を使用してバイト配列を登録する
	 */
	public DocumentListEntry insertNewDocument(String title, String url, byte[] content, String contentType)
	throws IOException, ServiceException {

		MediaByteArraySource mediaSource = new MediaByteArraySource(content, contentType);
		DocumentListEntry entry = service.insert(new URL(url), DocumentListEntry.class, mediaSource);

		entry.setTitle(new PlainTextConstruct(title));
		return entry.update();
	}

	/*
	 * Resume Upload
	 */
	public void resumeUpload(File file, DocumentListEntry folder, String title, ExecutorService executor, 
			ProgressListener listener, String uploaderKey, boolean isNotConvert)
	throws IOException, ServiceException {
		resumeUpload(file, folder, title, executor, listener, uploaderKey, isNotConvert, 
				DEFAULT_CHUNK_SIZE, PROGRESS_UPDATE_INTERVAL);
	}

	/*
	 * Resume Upload
	 */
	public void resumeUpload(File file, DocumentListEntry folder, String title, String mimeType, 
			ExecutorService executor, ProgressListener listener, String uploaderKey, boolean isNotConvert)
	throws IOException, ServiceException {
		resumeUpload(file, folder, title, mimeType, executor, listener, uploaderKey, isNotConvert, 
				DEFAULT_CHUNK_SIZE, PROGRESS_UPDATE_INTERVAL);
	}

	/*
	 * Resume Upload
	 */
	public void resumeUpload(File file, DocumentListEntry folder, String title, ExecutorService executor, 
			ProgressListener listener, String uploaderKey, boolean isNotConvert, int chunkSize, int interval)
	throws IOException, ServiceException {
		MediaFileSource mediaFile = getMediaFileSource(file);
		resumeUpload(mediaFile, folder, title, executor, listener, uploaderKey, isNotConvert, chunkSize, interval);
	}
	
	/*
	 * Resume Upload
	 */
	public void resumeUpload(File file, DocumentListEntry folder, String title, String mimeType,
			ExecutorService executor, ProgressListener listener, String uploaderKey, boolean isNotConvert, 
			int chunkSize, int interval)
	throws IOException, ServiceException {
		MediaFileSource mediaFile = getMediaFileSource(file, mimeType);
		resumeUpload(mediaFile, folder, title, executor, listener, uploaderKey, isNotConvert, chunkSize, interval);
	}
	
	/*
	 * Resume Upload
	 */
	public void resumeUpload(MediaFileSource mediaFile, DocumentListEntry folder, String title, 
			ExecutorService executor, ProgressListener listener, String uploaderKey, boolean isNotConvert, 
			int chunkSize, int interval)
	throws IOException, ServiceException {
		// Create {@link ResumableGDataFileUploader} for each file to upload

		String urlStr = getResumeUploadFolderUrl(folder, isNotConvert);

		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				ResumableGDataFileUploader uploader = new ResumableGDataFileUploader.Builder(
									service, new URL(urlStr), mediaFile, null /*empty meatadata*/)
						.title(title)
						.chunkSize(chunkSize).executor(executor)
						.trackProgress(listener, interval)
						.build();

				// attach the listener to list of uploaders
				if (listener instanceof FileUploadProgressListener) {
					((FileUploadProgressListener)listener).listenTo(uploader, uploaderKey, mediaFile);
				}

				// Start the upload
				uploader.start();

				break;

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
	}

	public MediaFileSource getMediaFileSource(String fileName) {
		return getMediaFileSource(fileName, MIME_TYPE_PDF);
	}

	public MediaFileSource getMediaFileSource(String fileName, String mimeType) {
		File file = new File(fileName);
		return getMediaFileSource(file, mimeType);
	}

	public MediaFileSource getMediaFileSource(File file) {
		return getMediaFileSource(file, MIME_TYPE_PDF);
	}

	public MediaFileSource getMediaFileSource(File file, String mimeType) {
		//MediaFileSource mediaFile = new MediaFileSource(file,
		//		DocumentListEntry.MediaType.fromFileName(file.getName())
		//		.getMimeType());
		MediaFileSource mediaFile = new MediaFileSource(file, mimeType);
		return mediaFile;
	}
	
	public ExecutorService getExecutor() {
		return getExecutor(MAX_CONCURRENT_UPLOADS);
	}
	
	public ExecutorService getExecutor(int count) {
		return Executors.newFixedThreadPool(count);
	}
	
	public FileUploadProgressListener getProgressListener() {
		return new FileUploadProgressListener();
	}
	
	public FileUploadProgressListener getProgressListener(boolean isUploadedEntry) {
		return new FileUploadProgressListener(isUploadedEntry);
	}
	
	public String getFilePath(DocumentListEntry folder, String title) {
		StringBuffer buf = new StringBuffer();
		if (folder != null) {
			buf.append(folder.getTitle().getPlainText());
		}
		buf.append(title);
		
		return buf.toString();
	}
	
	public AclFeed retrieveAcl(DocumentListEntry entry) throws IOException, ServiceException {
		return retrieveAcl(new URL(entry.getAclFeedLink().getHref()));
	}
	
	public AclFeed retrieveAcl(URL url) 
	throws IOException, ServiceException {
		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				return service.getFeed(url, AclFeed.class);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}
	
	public AclEntry grantAcl(DocumentListEntry entry, AclRole role, 
			AclScope.Type scopeType, String scopeValue) throws IOException, ServiceException {
		String urlStr = entry.getAclFeedLink().getHref();
		AclScope scope = new AclScope(scopeType, scopeValue);
		AclEntry aclEntry = new AclEntry();
		aclEntry.setRole(role);
		aclEntry.setScope(scope);
		return grantAcl(new URL(urlStr), aclEntry);
	}
	
	public AclEntry grantAcl(URL url, AclEntry aclEntry) 
	throws IOException, ServiceException {
		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				return service.insert(url, aclEntry);

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				// タイムアウトの場合、処理が継続されているのでリトライしない
				if (e.getMessage().indexOf(TIMEOUT_MESSAGE) > -1) {
					return null;
				}
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		
		return null;
	}
	
	public void revokeAcl(DocumentListEntry entry, AclScope.Type scopeType, String scopeValue) 
	throws IOException, ServiceException {
		String urlStr = entry.getAclFeedLink().getHref();
		AclScope scope = new AclScope(scopeType, scopeValue);
		revokeAcl(new URL(urlStr), scope);
	}
	
	public void revokeAcl(URL url, AclScope aclScope) 
	throws IOException, ServiceException {
		if (service == null) {
			login();
		}

		if (connectTimeout > 0) {
			service.setConnectTimeout(connectTimeout);
		}
		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				service.delete(url, aclScope);
				break;

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
	}
	
	/*
	 * Googleドキュメントのメタデータを更新する
	 */
	public DocumentListEntry updateMetadata(DocumentListEntry entry) 
	throws IOException, ServiceException {

		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				return entry.update();

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				// タイムアウトの場合、処理が継続されているのでリトライしない
				if (e.getMessage().indexOf(TIMEOUT_MESSAGE) > -1) {
					return null;
				}
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
		return null;
	}
		
	/*
	 * 強制更新
	 */
	public DocumentListEntry forceUpdateMetadata(DocumentListEntry entry) 
	throws IOException, ServiceException {
		String etag = entry.getEtag();
		try {
			service.getRequestFactory().setHeader("If-Match", "*");
			entry.setEtag(null);
			return updateMetadata(entry);

		} catch (ServiceException e) {
			service.getRequestFactory().setHeader("If-Match", etag);
			entry.setEtag(etag);
			return updateMetadata(entry);
			
		} finally {
			service.getRequestFactory().setHeader("If-Match", null);
		}
	}

	/*
	 * Googleドキュメントをゴミ箱に入れる
	 */
	public void trashDocument(DocumentListEntry entry) 
	throws IOException, ServiceException {

		for (int r = 0; r < NUM_RETRIES; r++) {
			try {
				entry.delete();
				break;

			} catch (IOException e) {
				logger.info("IOException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);

			} catch(SessionExpiredException e) {
				logger.info("SessionExpiredException:"+e.getMessage());
				login();

			} catch (ServiceException e) {
				logger.info("ServiceException:"+e.getMessage());
				if (r == (NUM_RETRIES - 1)) {
					throw e;
				}
				sleep(SLEEP_MILL_SECOND);
			}
		}
	}
	
	public int getConnectTimeout() {
		return this.connectTimeout;
	}
	
	public void setConnectTimeout(int second) {
		this.connectTimeout = second;
	}

	public synchronized void sleep(long msec) {
		try {
			wait(msec);
		} catch (InterruptedException e) {
		}
	}

}
